// Copyright 2016 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>

#include <fbl/algorithm.h>
#include <fbl/unique_fd.h>
#include <zircon/syscalls.h>

#include "filesystems.h"
#include "misc.h"

#define MB (1 << 20)
#define PRINT_SIZE (MB * 100)

namespace {

enum MountType {
  DoRemount,
  DontRemount,
};

// Test writing as much as we can to a file until we run
// out of space
template <MountType mt>
bool test_maxfile(void) {
  BEGIN_TEST;

  if (!test_info->can_be_mounted && mt == DoRemount) {
    fprintf(stderr, "Filesystem cannot be mounted; cannot remount\n");
    return true;
  }

  // TODO(ZX-1735): We avoid making files that consume more than half
  // of physical memory. When we can page out files, this restriction
  // should be removed.
  const size_t physmem = zx_system_get_physmem();
  const size_t max_cap = physmem / 2;

  fbl::unique_fd fd(open("::bigfile", O_CREAT | O_RDWR, 0644));
  ASSERT_TRUE(fd);
  char data_a[8192];
  char data_b[8192];
  char data_c[8192];
  memset(data_a, 0xaa, sizeof(data_a));
  memset(data_b, 0xbb, sizeof(data_b));
  memset(data_c, 0xcc, sizeof(data_c));
  size_t sz = 0;
  ssize_t r;

  auto rotate = [&](const char* data) {
    if (data == data_a) {
      return data_b;
    } else if (data == data_b) {
      return data_c;
    } else {
      return data_a;
    }
  };

  const char* data = data_a;
  for (;;) {
    if (sz >= max_cap) {
      fprintf(stderr, "Approaching physical memory capacity: %zd bytes\n", sz);
      r = 0;
      break;
    }

    if ((r = write(fd.get(), data, sizeof(data_a))) < 0) {
      fprintf(stderr, "bigfile received error: %s\n", strerror(errno));
      if ((errno == EFBIG) || (errno == ENOSPC)) {
        // Either the file should be too big (EFBIG) or the file should
        // consume the whole volume (ENOSPC).
        fprintf(stderr, "(This was an expected error)\n");
        r = 0;
      }
      break;
    }
    if ((sz + r) % PRINT_SIZE < (sz % PRINT_SIZE)) {
      fprintf(stderr, "wrote %lu MB\n", (sz + r) / MB);
    }
    sz += r;
    ASSERT_EQ(r, sizeof(data_a));

    // Rotate which data buffer we use
    data = rotate(data);
  }
  ASSERT_EQ(r, 0, "Saw an unexpected error from write");
  fprintf(stderr, "wrote %lu bytes\n", sz);

  struct stat buf;
  ASSERT_EQ(fstat(fd.get(), &buf), 0, "Couldn't stat max file");
  ASSERT_EQ(buf.st_size, static_cast<ssize_t>(sz), "Unexpected max file size");

  // Try closing, re-opening, and verifying the file
  ASSERT_EQ(close(fd.release()), 0);
  if (mt == DoRemount) {
    ASSERT_TRUE(check_remount(), "Could not remount filesystem");
  }
  fd.reset(open("::bigfile", O_RDWR, 0644));
  ASSERT_TRUE(fd);
  ASSERT_EQ(fstat(fd.get(), &buf), 0, "Couldn't stat max file");
  ASSERT_EQ(buf.st_size, static_cast<ssize_t>(sz), "Unexpected max file size");
  char readbuf[8192];
  size_t bytes_read = 0;
  data = data_a;
  while (bytes_read < sz) {
    r = read(fd.get(), readbuf, sizeof(readbuf));
    ASSERT_EQ(r, static_cast<ssize_t>(fbl::min(sz - bytes_read, sizeof(readbuf))));
    ASSERT_EQ(memcmp(readbuf, data, r), 0, "File failed to verify");
    data = rotate(data);
    bytes_read += r;
  }

  ASSERT_EQ(bytes_read, sz);

  ASSERT_EQ(unlink("::bigfile"), 0);
  ASSERT_EQ(close(fd.release()), 0);
  END_TEST;
}

// Test writing to two files, in alternation, until we run out
// of space. For trivial (sequential) block allocation policies,
// this will create two large files with non-contiguous block
// allocations.
template <MountType mt>
bool TestZippedMaxfiles(void) {
  BEGIN_TEST;

  if (!test_info->can_be_mounted && mt == DoRemount) {
    fprintf(stderr, "Filesystem cannot be mounted; cannot remount\n");
    return true;
  }

  // TODO(ZX-1735): We avoid making files that consume more than half
  // of physical memory. When we can page out files, this restriction
  // should be removed.
  const size_t physmem = zx_system_get_physmem();
  const size_t max_cap = physmem / 4;

  fbl::unique_fd fda(open("::bigfile-A", O_CREAT | O_RDWR, 0644));
  fbl::unique_fd fdb(open("::bigfile-B", O_CREAT | O_RDWR, 0644));
  ASSERT_TRUE(fda);
  ASSERT_TRUE(fdb);
  char data_a[8192];
  char data_b[8192];
  memset(data_a, 0xaa, sizeof(data_a));
  memset(data_b, 0xbb, sizeof(data_b));
  size_t sz_a = 0;
  size_t sz_b = 0;
  ssize_t r;

  size_t* sz = &sz_a;
  int fd = fda.get();
  const char* data = data_a;
  for (;;) {
    if (*sz >= max_cap) {
      fprintf(stderr, "Approaching physical memory capacity: %zd bytes\n", *sz);
      r = 0;
      break;
    }

    if ((r = write(fd, data, sizeof(data_a))) <= 0) {
      fprintf(stderr, "bigfile received error: %s\n", strerror(errno));
      // Either the file should be too big (EFBIG) or the file should
      // consume the whole volume (ENOSPC).
      ASSERT_TRUE(errno == EFBIG || errno == ENOSPC);
      fprintf(stderr, "(This was an expected error)\n");
      break;
    }
    if ((*sz + r) % PRINT_SIZE < (*sz % PRINT_SIZE)) {
      fprintf(stderr, "wrote %lu MB\n", (*sz + r) / MB);
    }
    *sz += r;
    ASSERT_EQ(r, sizeof(data_a));

    fd = (fd == fda.get()) ? fdb.get() : fda.get();
    data = (data == data_a) ? data_b : data_a;
    sz = (sz == &sz_a) ? &sz_b : &sz_a;
  }
  fprintf(stderr, "wrote %lu bytes (to A)\n", sz_a);
  fprintf(stderr, "wrote %lu bytes (to B)\n", sz_b);

  struct stat buf;
  ASSERT_EQ(fstat(fda.get(), &buf), 0, "Couldn't stat max file");
  ASSERT_EQ(buf.st_size, static_cast<ssize_t>(sz_a), "Unexpected max file size");
  ASSERT_EQ(fstat(fdb.get(), &buf), 0, "Couldn't stat max file");
  ASSERT_EQ(buf.st_size, static_cast<ssize_t>(sz_b), "Unexpected max file size");

  // Try closing, re-opening, and verifying the file
  ASSERT_EQ(close(fda.release()), 0);
  ASSERT_EQ(close(fdb.release()), 0);
  if (mt == DoRemount) {
    ASSERT_TRUE(check_remount(), "Could not remount filesystem");
  }
  fda.reset(open("::bigfile-A", O_RDWR, 0644));
  fdb.reset(open("::bigfile-B", O_RDWR, 0644));
  ASSERT_TRUE(fda);
  ASSERT_TRUE(fdb);

  char readbuf[8192];
  size_t bytes_read_a = 0;
  size_t bytes_read_b = 0;

  fd = fda.get();
  data = data_a;
  sz = &sz_a;
  size_t* bytes_read = &bytes_read_a;
  while (*bytes_read < *sz) {
    r = read(fd, readbuf, sizeof(readbuf));
    ASSERT_EQ(r, static_cast<ssize_t>(fbl::min(*sz - *bytes_read, sizeof(readbuf))));
    ASSERT_EQ(memcmp(readbuf, data, r), 0, "File failed to verify");
    *bytes_read += r;

    fd = (fd == fda.get()) ? fdb.get() : fda.get();
    data = (data == data_a) ? data_b : data_a;
    sz = (sz == &sz_a) ? &sz_b : &sz_a;
    bytes_read = (bytes_read == &bytes_read_a) ? &bytes_read_b : &bytes_read_a;
  }

  ASSERT_EQ(bytes_read_a, sz_a);
  ASSERT_EQ(bytes_read_b, sz_b);

  ASSERT_EQ(unlink("::bigfile-A"), 0);
  ASSERT_EQ(unlink("::bigfile-B"), 0);
  ASSERT_EQ(close(fda.release()), 0);
  ASSERT_EQ(close(fdb.release()), 0);

  END_TEST;
}

// Disk must be at least this large to exceed the maximum transaction limit during delayed data
// allocation on non-FVM-backed Minfs partitions.
const test_disk_t disk = {
    .block_count = 1LLU << 20,
    .block_size = 1LLU << 9,
    .slice_size = 1LLU << 23,
};

}  // namespace

RUN_FOR_ALL_FILESYSTEMS_SIZE(maxfile_tests, disk,
                             RUN_TEST_LARGE((test_maxfile<DontRemount>))
                                 RUN_TEST_LARGE((test_maxfile<DoRemount>))
                                     RUN_TEST_LARGE((TestZippedMaxfiles<DontRemount>))
                                         RUN_TEST_LARGE((TestZippedMaxfiles<DoRemount>)))
